⚛ AsyncBoundary의 상태별 테스트

React-Query, Suspense, ErrorBoundary를 모두 합쳐서 하나의 AsyncBoundary라는 컴포넌트를 이전에 만들었다.

해당 컴포넌트에서는 React-Query를 통해 3가지의 상태를 가질 수 있었다.

  1. loading : 데이터가 fetching중인 상태
  2. error : 하위 컴포넌트 마운트 등의 lifeCycle중 발생한 에러 핸들링
  3. success : 데이터 fetching 완료

필요에 따라서는 error 상태 이후, retry 기능도 가능하도록 하였으니 이 또한 확인이 필요하다.

mocking 기반으로 setting

fallback컴포넌트와 useQuery는 속성으로 받거나 내부적으로 포함하고 있고, data fetching 진행 시간을 테스트에서 실제로 소비할 이유는 없기 때문에, mocking하기로 하였다.

SuspenseFallback

const SuspenseFallback = () => <span data-testid="loading">로딩중</span>

로딩 상태를 표시하는 fallback 컴포넌트이다.AsyncBoundary에서 상위 컴포넌트의 요청대로 rendering 하기 때문에 모두 다르겠지만, 로딩중이라는 표시는 모두 동일함으로 최대한 간단하게 구성하였다.

ErrorFallback

const ErrorFallback = ({
  error,
  resetBoundary,
}: {
  error?;
  resetBoundary?;
}) => {
  return (
    <>
      <span data-testid="error-message">{error.message}</span>
      <button data-testid="retry-button" onClick={resetBoundary}>
        재시도
      </button>
    </>
  );
};

에러 상태를 표시하는 fallback 컴포넌트이다. AsyncBoundary에서 error와 저장되어있는 queryCache 삭제 및 AsyncBoundary의 상태값을 변경하여 rerendering을 시키는 resetBoundary라는 속성들을 받아와서 사용한다.

실행할때마다 다른 key를 갖고있는 렌더링

const renderAsyncBoundary = (key, mock) => {
  const Component = () => {
    useQuery(key, () => mock())
    return <span data-testid="fetched-data">성공</span>
  }

  return (
    <AsyncBoundary
      errorFallback={<ErrorFallback />}
      suspenseFallback={<SuspenseFallback />}
    >
      <Component />
    </AsyncBoundary>
  )
}

return되는 부분은 모두 동일하여 여러번 쓸 필요 없이 하나의 함수로 관리하도록 하였다.

또한, useQuery 자체가 key가 동일하다면 비동기작업을 실행하지 않고 저장되어있는 값을 가져오기 때문에, key를 바꿔주지않으면 이전 테스트의 영향으로 다음 테스트가 원활히 진행되는것 같지 않았다.

따라서, 테스트 마다 구분이 되는 key 받아오도록 하였다.

mocking 함수 또한, 로딩, 에러, 재시도mockRejectedValueOnce, mockResolvedValueOnce가 다르게 구성되기 때문에, 이전에 만들어서 렌더링하도록 하였다.

테스트코드 작성

로딩

it('로딩', async () => {
  const { getByTestId } = render(renderAsyncBoundary('로딩', jest.fn()))
  await waitFor(() => expect(getByTestId('loading')).toBeTruthy())
})

로딩 이라는 키를 받았을 때, loading화면이 잘 노출되는지 테스트하는 과정이였다.

딱히 문제될 것은 없었기 때문에 넘어가자

에러

describe('에러 발생', () => {
  // 에러 발생과 관련된 테스트
})

에러가 발생했을 때에는 에러 핸들링재시도를 요청했을 때의 결과가 잘 나오는지 테스트를 해봐야 했기 때문에, 하나의 에러 발생으로 묶었다.

에러 핸들링

describe('에러 발생', () => {
  it('에러 핸들링', async () => {
    const mock = jest.fn().mockRejectedValue(new Error('에러'))
    const { getByTestId } = render(renderAsyncBoundary('에러', mock))

    await waitFor(() => expect(getByTestId('error-message')).toBeTruthy())
  })
})

실행을 하였을 때, 비동기 Error를 반환하는 mocking함수를 만들었다.

해당 비동기 함수를 사용하게 되는 컴포넌트에서 사용을 할 때 Error를 발생할 것이다.

또한 이전 로딩 키와 다른 에러 키를 사용하여 useQuery가 다른 키로 인식해 새롭게 작동되도록 해준다.

에러 발생 후 retry

describe('에러 발생', () => {
  it('에러 핸들링', async () => {
    const mock = jest.fn().mockRejectedValue(new Error('에러'))
    const { getByTestId } = render(renderAsyncBoundary('에러', mock))

    await waitFor(() => expect(getByTestId('error-message')).toBeTruthy())
  })

  it('에러 발생 후, retry', async () => {
    const mock = jest
      .fn()
      .mockRejectedValueOnce(new Error('에러'))
      .mockResolvedValueOnce({ data: '성공' })

    const { getByTestId } = render(renderAsyncBoundary('retry', mock))

    await waitFor(() => expect(getByTestId('retry-button')).toBeTruthy())

    fireEvent.click(getByTestId('retry-button'))

    await waitFor(() => expect(getByTestId('loading')).toBeTruthy())
    await waitFor(() => expect(getByTestId('fetched-data')).toBeTruthy())
  })
})

에러가 발생하고 재시도를 테스트하는 부분을 추가한다.

이번의 mocking함수는 첫 요청에는 에러를 반환하지만, 두번째 요청에는 성공하도록 작성한다.

당연히, key도 변경해서 render를 해준다.

  1. 에러가 발생해서 retry-button이 화면에 그려짐
  2. retry-button을 클릭하는 이벤트를 실행
  3. loading 화면이 그려짐
  4. 성공으로 fetched-data 화면이 그려짐

Success

it('데이터 패칭 완료', async () => {
  const mock = jest.fn().mockResolvedValueOnce({ data: '성공' })

  const { getByTestId } = render(renderAsyncBoundary('완료', mock))

  await waitFor(() => expect(getByTestId('loading')).toBeTruthy())
  await waitFor(() => expect(getByTestId('fetched-data')).toBeTruthy())
})

데이터 패칭이 성공했을 때의 테스트이다. 당연히 성공을 반환하는 mocking함수를 작성해준다.

크게 문제될 부분은 없었다.

테스트 결과

1

맞게 테스트한것인지는 모르겠지만, 최대한 모든 상황을 테스트해보려하고, mocking을 사용하여 적은 시간이 들도록 작성하였다.


@SangMin
👆 H'e'story

🚀GitHub